# -*- coding:utf-8 -*-

import argparse
import json
import logging
import os
import shutil
import subprocess
import sys
import time

import cv2
import numpy as np
import xlrd  # xlrd=1.2.0
from xlrd.sheet import Sheet
from PIL import Image, ImageDraw, ImageFont

from modules.devices import RK3568

# color: BGR
GREEN  = (0, 255, 0)
RED    = (0, 0, 255)
PURPLE = (164, 73, 163)
# every step delay time for rk3568
RK3568_DELAY = 1.5 # s

def argumnet_parse():
    parser = argparse.ArgumentParser(description='manual to this script')
    parser.add_argument('-c', '--case-path', required=True, help='input testcase absolute path.')
    parser.add_argument('-s', '--shift-tolerance', type=int, default=150, help='tolerable shift of pic diff area.')
    parser.add_argument('-r', '--retry', type=int, default=0, help='retry times if run testcase fail')
    parser.add_argument('-t', '--reset-time', type=int, default=0, help='if set time greater than 0, test device will reset before run test.')
    parser.add_argument('-f', '--fixed-app', action='store_true', help='fixed app or not')
    parser.add_argument('-g', '--gen-std-pic', action='store_true', help='generate standard picture')
    parser.add_argument('-0', '--is-top0', action='store_true', help='excute top0 or top1')
    parser.add_argument('-d', '--device-num', default='default', help='specify a test device')
    
    args = parser.parse_args()
    args.case_path = os.path.normpath(args.case_path)
    
    if args.device_num == 'default':
        args.device_num = get_device_num()
        print_to_log(f'device num: {args.device_num}')
    return args


def env_check() -> bool:
    os.system('hdc kill; hdc start')
    res = subprocess.check_output(['hdc', '--version'], encoding='utf-8')
    if res[0:3] == 'Ver':
        return True
    else:
        raise Exception('hdc --version 返回结果有误，请检查。')


def get_device_num():
    get_device_list = subprocess.run(['hdc', 'list', 'targets'], capture_output=True)
    device_list = get_device_list.stdout.decode('UTF-8').split()
    print_to_log(device_list)
    device_num = device_list[0]
    if '[Empty]' in device_num:
        raise Exception('没检测到设备，请检查连接情况！')
    return device_num


def print_to_log(str, level='time'):
    log = logging.getLogger(log_path)
    log.handlers.clear()
    
    if level == "notime":
        format_str = logging.Formatter(fmt='%(message)s')
    else:
        format_str = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
    
    log.setLevel(level=logging.INFO)
    if not log.handlers:
        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(logging.INFO)
        stream_handler.setFormatter(format_str)
        log.addHandler(stream_handler)
        
        file_handler = logging.FileHandler(log_path, encoding='utf-8')
        file_handler.setLevel(logging.INFO)
        file_handler.setFormatter(format_str)
        log.addHandler(file_handler)
    
    log.info(str)


def execute(cmd, desc='', print_res=True):
    if cmd == "":
        return
    if print_res:
        print_to_log(f'{desc}: {cmd}', 'notime')   
    os.system(cmd)     


def scan_all_dirs(target):
    case_file_list = []
    case_file_dict = {}
    for root, dirs, files in os.walk(target):
        for f in files:
            if f.endswith('.xlsx') and \
                root.find('Z-SKIP_CASE') == -1 and \
                root.find('AppTest-template') == -1:
                case_file = os.path.join(root, f)
                case_name = os.path.basename(os.path.dirname(case_file))
                case_file_list.append(case_file)
                case_file_dict[case_name] = case_file
                break
    case_file_list.sort()
    return case_file_list, case_file_dict


def install_weekly_app(is_install:bool):
    if not is_install:
        return
    print_to_log('正在安装 weekly app...')
    execute(f'hdc -t {device_num} shell mount -rw -o remount /')
    execute('hdc file send resources\\app.tar /data/')
    execute(f'hdc -t {device_num} shell "cd /system;tar -vfx /data/app.tar"')
    execute(f'hdc -t {device_num} shell rm -rf /data/app.tar')
    
    execute(f'hdc -t {device_num} shell mount -rw -o remount /sys_prod')
    execute('hdc file send resources\\app_prod.tar /data/')
    execute(f'hdc -t {device_num} shell "cd /data;tar -vfx app_prod.tar -C /sys_prod/"')
    execute(f'hdc -t {device_num} shell rm -rf /data/app_prod.tar')
    
    print_to_log('>>>>>>>>>>>>> 结束，操作完成')
    return 0


def get_case_list(args_case_path:str):
    case_list = []
    all_case_dict = {}
    case_list, all_case_dict = scan_all_dirs(testcase_path)
    # if os.path.basename(args_case_path)[0:8] == 'TestCase' and \
    #     os.path.dirname(args_case_path) == os.path.dirname(cwd):
        # case_list, case_dict = scan_all_dirs(testcase_path)
    if args_case_path.find('TestCase') != -1 and \
        os.path.basename(args_case_path)[0:8] != 'TestCase':
        case_list, _ = scan_all_dirs(args_case_path)
    return case_list, all_case_dict


def reset(t):
    if t <= 0:
        return
    device.reset()
    time.sleep(t)
    execute('hdc kill; hdc start')


def read_run_map():
    run_map_path = os.path.join(cwd, 'RunMap.json')
    if os.path.exists(run_map_path):
        os.remove(run_map_path)
    execute(f'hdc -t {device_num} file recv /data/RunMap.json')
    if os.path.exists(run_map_path):
        with open('RunMap.json', 'r') as fp:
            dic = json.load(fp)
    else:
        dic = {}
    return dic


def generate_log_path(case, retry):
    relative_path = os.path.relpath(os.path.dirname(case), start=os.path.dirname(cwd))
    relative_log_path = os.path.join(os.path.dirname(cwd), 'log', relative_path)
    test_name = f'{retry}_{os.path.basename(relative_log_path)}'
    return os.path.join(relative_log_path, test_name)


def path_splicing(str):
    split_str = str.split('log' + os.sep)[-1]
    split_list = split_str.split(os.sep)
    splicing_str = ''
    for i in range(1, len(split_list)):
        splicing_str = os.path.join(splicing_str, split_list[i - 1])
    splicing_str = os.path.join('..', splicing_str)
    return splicing_str


def analysis_log(report):
    case_num_list = []
    case_result_list = []
    step_error_list = []
    step_warning_list = []
    cmp_list = []
    step_num = ''
    tmp_step = ''
    warning_list_str = ''
    repetitive_case = ''
    with open(report, mode='r', encoding='utf-8', errors='ignore') as file:
        for line in file.readlines():
            case_name = ''
            case_result = ''
            error_step = '/'
            warning_step = '/'
            cmp_dir = '/'
            if "执行用例" in line:
                case_name = line.split(os.sep)[-2]
                if repetitive_case != case_name:
                    repetitive_case = case_name
                else:
                    case_num_list.pop()
                    case_result_list.pop()
                    step_error_list.pop()
                    step_warning_list.pop()
                    cmp_list.pop()
                case_num_list.append(case_name)
                case_result_list.append(case_result)
                step_error_list.append(error_step)
                step_warning_list.append(warning_step)
                cmp_list.append(cmp_dir)
            if "|| STEP" in line:
                step_num = line.split('||')[-2]
            if "对比失败^^^^" in line:
                if tmp_step != step_num:
                    warning_list_str = warning_list_str + " " + step_num
                    tmp_step = step_num
            if case_name in line and "动作结束" in line:
                cmp_dir = path_splicing(line)
                cmp_list[-1] = cmp_dir
                if "失败" in line:
                    step_error_list[-1] = step_num
            if case_name in line and "[执行结果]" in line:
                step_warning_list[-1] = warning_list_str
                if "successed" in line and "STEP" in warning_list_str:
                    case_result_list[-1] = "测试通过(部分对比区域失败)"
                elif "successed" in line and "STEP" not in warning_list_str:
                    case_result_list[-1] = "测试通过"
                elif "blocked" in line:
                    case_result_list[-1] = "阻塞"
                elif "failed" in line:
                    case_result_list[-1] = "失败"
                step_num = ''
                tmp_step = ''
                warning_list_str = ''
    file.close()
    return case_num_list, case_result_list, step_error_list, step_warning_list, cmp_list


def table_handle(type, res_list, result, is_retry=False):
    table_th=''
    for title in res_list[0]:
        table_th = table_th + '<th>'+ str(title) + '</th>'
    table_th='<tr>' + table_th + '</tr>\n'

    #处理行格式
    table_tr = ''

    for i in range(0, len(res_list)):
        for n,m in enumerate(res_list[i]):
            if n == 0:
                #行首单独处理<tr>
                res_list[i][m] = '<tr><td>' + str(res_list[i][m])
            elif n == 5:
                case_name = ''
                if str(res_list[i][m]) != '/':
                    case_name = str(res_list[i][m]).split(os.sep)[-2]
                res_list[i][m] = """ <a href=%s target="_blank">%s</a> """%(str(res_list[i][m]), case_name)
            if res_list[i][m] == "测试通过":
                res_list[i][m] = '<td bgcolor="#00FF00">' + str(res_list[i][m])
            elif res_list[i][m] == "失败" or res_list[i][m] == "阻塞":
                res_list[i][m] = '<td bgcolor="#FF0000">' + str(res_list[i][m])
            elif res_list[i][m] == "测试通过(部分对比区域失败)":
                res_list[i][m] = '<td bgcolor="#FF8000">' + str(res_list[i][m])
            if n == 1:
            # if n >= 1 and n <= 3:
                table_tr = table_tr + str(res_list[i][m]) + '</td>'
            else:
                table_tr = table_tr + str(res_list[i][m]) + '</td><td>'
        #行尾单独处理</tr>
        table_tr = table_tr[0:-3] + '/tr>\n'

    if is_retry == False:
        table_body = """
        <h2 style='text-align:center'>预测试TOP%s用例执行报告</h1>
        <p style='text-align:center; font-size:20px'>%s</p>
        <body>
        <table border='2'>
        <caption>执行详情</caption>
        """%(type, result)
    else:
        table_body = """
        <h2 style='text-align:center'></h1>
        <p style='text-align:center; font-size:20px'>%s</p>
        <body>
        <table border='2'>
        <caption>执行详情</caption>
        """%(result)

    table_body = table_body + table_th + table_tr + '</table>\n</body>\n'
    return table_body


def html_output():
    case_type = 1
    if args.is_top0:
        case_type = 0
    
    case_names, case_result, step_error, step_warning, cmp_pic = analysis_log(log_path)
    case_num = len(case_names)
    ok_num = case_result.count("测试通过")
    half_ok_num = case_result.count("测试通过(部分对比区域失败)")
    block_num = case_result.count("阻塞")
    fail_num = case_num - ok_num - half_ok_num - block_num
    json_list = []
    for index, case in enumerate(case_names):
        json_dict = {"序号": index + 1, "用例分支": case, "执行结果": case_result[index], "致命失败步骤": step_error[index], "部分区域对比失败步骤": step_warning[index], "对比图": cmp_pic[index]}
        json_list.append(json_dict)

    std_list = []
    retry_list = []
    tmp_list = []
    once_ok = 0
    once_half_ok = 0
    once_block = 0
    once_fail = 0
    for i in range(0, len(json_list)):
        tmp_list.append(json_list[i]['用例分支'])
        if tmp_list.count(tmp_list[i]) > 1:
            retry_list.append(json_list[i])
        else:
            std_list.append(json_list[i])
            if json_list[i]['执行结果'] == '测试通过':
                once_ok = once_ok + 1
            elif json_list[i]['执行结果'] == '测试通过(部分对比区域失败)':
                once_half_ok = once_half_ok + 1
            elif json_list[i]['执行结果'] == '阻塞':
                once_block = once_block + 1
            else:
                once_fail = once_fail + 1

    retry_ok = half_ok_num + ok_num - once_ok - once_half_ok
    overall_result = "预测试用例分支共执行{}个，成功{}个，阻塞{}个，失败{}个；重试用例{}个，重试成功{}个".format(len(std_list), once_ok + once_half_ok , once_block, once_fail, len(retry_list), retry_ok)

    table_head="<!DOCTYPE html>\n" \
               "<html>\n" \
               "<head>\n" \
               "<meta charset='UTF-8'>\n" \
               "<title>预测试用例执行报告</title>\n" \
               "<style type='text/css'>\n" \
               "table {color:#333333; width:75%; border-collapse:collapse; text-align:left; margin: auto;}\n" \
               "table th {background-color:#97CEFA; padding:8px; border-color:#97CEFA;}\n" \
               "table td {padding:8px; border-color:#97CEFA;}\n" \
               "</style>\n" \
               "</head>\n"

    std_table_body = table_handle(case_type, std_list, overall_result)
    if len(retry_list) > 0:
        retry_result = "重试用例共执行{}个，成功{}个，阻塞{}个，失败{}个".format(len(retry_list), retry_ok, block_num - once_block, fail_num - once_fail)
        retry_table_body = table_handle(case_type, retry_list, retry_result, True)
        tableCode = table_head + std_table_body + retry_table_body + '</html>'
    else:
        tableCode = table_head + std_table_body + '</html>'

    html_write=open(html_path,"w",encoding="utf-8")
    html_write.write(tableCode)


def save_run_map_to_device(dic):
    with open('RunMap.json', 'w') as fp:
        json.dump(dic, fp)
    execute(f'hdc -t {device_num} shell send RunMap.json /data/')
    if os.path.exists(os.path.join(cwd, 'RunMap.json')):
        os.remove(os.path.join(cwd, 'RunMap.json'))


def run_dep_case(deps, case_name, case_path, run_map, failed_retry):
    if len(deps) == 1 and deps[0] == '': 
        return True
    for case in deps:
        if run_map.get(case) == 'successed':
            continue
        if run_map.get(case) == None:
            print_to_log('\n\n--------------------------------------------------------------------------------------')
            print_to_log(f'          执行用例 {case_path}')
            print_to_log('--------------------------------------------------------------------------------------')
            ret = run_case(all_case_dict[case], run_map, failed_retry)
            if ret == False:
                run_map[case_name] = 'blocked: ' + case
                print_to_log(f'\n++++++++++++++++++++++++++++++[执行结果] 状态：{run_map[case_name]}，用例 {case_name} 失败！！！ +++++++++++++++++++++++++')
                save_run_map_to_device(run_map)
                return False
    return True


def set_log():
    '''基本没用，暂时关闭'''
    return
    print_to_log('\n=======================开启日志====================')
    execute(f'hdc -t {device_num} shell hilog -w stop')
    execute(f'hdc -t {device_num} shell rm /data/log/faultlog/temp/*;rm /data/log/faultlog/faultlogger/*;rm /data/log/hilog/*')
    execute(f'hdc -t {device_num} shell "hilog -b D; hilog -p off; hilog -Q domainoff; hilog -Q pidoff; hilog -r; hilog -w start -l 100M -n 100"')


def get_log(path):
    '''基本没用，暂时关闭'''
    return
    print_to_log('\n=======================收集日志====================')
    execute(f'hdc -t {device_num} shell hilog -w stop; tar -cvf data/hilog.tar data/log/hilog')
    execute(f'hdc file recv data/hilog.tar {path}')
    execute(f'hdc -t {device_num} shell rm data/hilog.tar; tar -cvf data/faultlog.tar data/log/faultlog')
    execute(f'hdc file recv data/faultlog.tar {path}')
    execute(f'hdc -t {device_num} shell rm data/faultlog.tar')


def get_cmp_area(std_pic_path):
    std_pic = cv2.imread(std_pic_path)
    
    sH, sW, _ = std_pic.shape
    if sH < sW:
        std_pic = cv2.transpose(std_pic)
    
    lower = np.array([164, 73, 163]) #BGR 识别紫色框的颜色下限
    upper = np.array([164, 73, 163]) #BGR 识别紫色框的颜色上限
    # mask 将在上面颜色范围的区域变成白色，其他区域变成黑色
    # 将紫色边框区域变为白色，其他区域变为黑色
    mask = cv2.inRange(std_pic, lower, upper)
    
    # 变成二值图像放在binary中
    # _, binary = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) # 多余？
    
    # 在binnary中发现轮廓，轮廓按照面积从小到大排列
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    cmp_area_array = []
    for i in range(len(contours) - 1, -1, -1): # 逆向遍历所有的轮廓
        x, y, w, h = cv2.boundingRect(contours[i]) # 将轮廓分解为识别对象的左上角坐标和宽、高
        
        # 判断是否识别到方框。防止图片上出现与方框相同颜色的区域的干扰。
        if mask[y, x] != 255 or mask[y + 8, x + 8] != 0: # 8, 任意，只要在方框内就行
            continue
        if mask[y + h - 1, x + w -1] != 255 or mask[y + h - 8, x + w - 8] != 0:
            continue
        if mask[y, x + w - 1] != 255 or mask[y + 8, x + w - 8] != 0:
            continue
        if mask[y + h - 1, x] != 255 or mask[y + h - 8, x + 8] != 0:
            continue
        if w < 15 or h < 15:
            continue
        
        # 保存为左上角、右下角的格式
        cmp_area_array.append([x + 5, y + 5, x + w - 6, y + h - 6]) # 边框宽度为5

    return cmp_area_array


def draw_stdpic_border(std_pic, top_left:tuple, btm_right:tuple, color=PURPLE):
    '''生成标准图的紫色矩形框。\n
    top_left/btm_right: 均为包括5像素边框的含边界的坐标值。'''
    std_pic[top_left[1]:top_left[1]+5,     top_left[0]:btm_right[0]+1] = color # 上边框
    std_pic[btm_right[1]-4:btm_right[1]+1, top_left[0]:btm_right[0]+1] = color # 下边框
    std_pic[top_left[1]:btm_right[1]+1,     top_left[0]:top_left[0]+5] = color # 左边框
    std_pic[top_left[1]:btm_right[1]+1, btm_right[0]-4:btm_right[0]+1] = color # 右边框


def draw_top_bar(length, desc):
    '''绘制比较结果图的顶部说明栏。'''
    desc_font = ImageFont.truetype("simsun.ttc", 48)
    desc_img = np.zeros((130, length, 3), np.uint8)
    img_pil = Image.fromarray(desc_img)
    draw = ImageDraw.Draw(img_pil)
    draw.text((0, 80),  '下一步描述: '+desc.split('@NEXT@')[1], font=desc_font, fill=(255, 255, 255))
    draw.text((0, 10),  '当前步描述: '+desc.split('@NEXT@')[0], font=desc_font, fill=(255, 255, 255))
    return np.array(img_pil)

  
def pic_match_and_return_pos(std_pic_path:str, cmp_pic_path:str, check_loc_array,
                             result_file, threshold=0.004, next_step_point=(0, 0), binding_area=0, special_handle={},
                             shift_tolerance=0, desc='NO DESCRIBE@NO DESCRIBE', mask_array = []):
    if (not os.path.exists(std_pic_path)):
        print_to_log(f'标准图不存在：{std_pic_path}')
    if (not os.path.exists(cmp_pic_path)):
        print_to_log(f'对比图不存在：{cmp_pic_path}')

    std_pic = cv2.imread(std_pic_path)
    cmp_pic = cv2.imread(cmp_pic_path)
    # 保持图片方向一致(视频截图可能出现旋转状态的截图)
    sH, sW, _ = std_pic.shape
    cH, cW, _ = cmp_pic.shape 
    if sH < sW:
        std_pic = cv2.transpose(std_pic)
        sH, sW = sW, sH
    if cH < cW:
        cmp_pic = cv2.transpose(cmp_pic)
        cH, cW = cW, cH
    
    diffCnt = 0
    miss_next_step = False
    majorAreaShift = (0, 0)
    majorAreaIsExist = False
    save_as_std = cmp_pic.copy()
    font = cv2.FONT_HERSHEY_COMPLEX

    if len(check_loc_array) < 1:
        print_to_log("ERROR: Input params invaild, Picture Match Only Support One Location!!!")
        return
    # 依次处理每一个要对比的区域
    for index, loc in enumerate(check_loc_array):
        # 获取实际比较的区域
        if next_step_point != (0, 0) and threshold != 0.004: # 非平移相等
            v = shift_tolerance
            sY1 = 0 if loc[1] <= v else loc[1] - v
            sY2 = device.H if loc[3] + v > device.H else loc[3] + v
            sX1 = 0 if loc[0] <= v else loc[0] - v
            sX2 = device.W if loc[2] +v > device.W else loc[2] + v
            crop_cmp_pic = cmp_pic[sY1:sY2+1, sX1:sX2+1]
        else:
            crop_cmp_pic = cmp_pic
        crop_std_pic = std_pic[loc[1] : loc[3] + 1, loc[0]: loc[2] + 1]
        height, width, _ = crop_std_pic.shape
        # 对比图片
        if len(mask_array) != 0:
            results = cv2.matchTemplate(crop_cmp_pic, crop_std_pic, cv2.TM_SQDIFF_NORMED, mask=mask_array[index])
        else:
            results = cv2.matchTemplate(crop_cmp_pic, crop_std_pic, cv2.TM_SQDIFF_NORMED)
        # 获取匹配结果中的最小值，最大值，最小坐标和最大坐标
        # cv2.TM_SQDIFF_NORMED 最小值为最佳匹配，最小坐标为最佳匹配处的坐标
        min_val, _, min_loc, _ = cv2.minMaxLoc(results)
        print_to_log(f"差异值: {min_val}")
        print_to_log(f"匹配到位置: {min_loc} {width} {height}")
        
        if next_step_point != (0, 0) and threshold != 0.004: # 非平移相等
            # 手动计算差异率
            _matched_area = crop_cmp_pic[min_loc[1] : min_loc[1] + height,
                                        min_loc[0] : min_loc[0] + width] ## 匹配到的区域
            data=np.abs((crop_std_pic.astype(np.int32) - _matched_area.astype(np.int32)))
            vcnt = (data > 30).any(axis=2).sum()
            value = vcnt / (height * width)
            print_to_log(f"检查位置{index}：{loc}， 向右向下偏移: {min_loc[0]-(loc[0]-sX1), min_loc[1]-(loc[1]-sY1)}， 差异率: {value:.2%} ")
        # 加上边框后的区域坐标
        if next_step_point != (0, 0) and threshold != 0.004: # 非平移相等
            top_left = (min_loc[0] + sX1 - 5, min_loc[1] + sY1 - 5) # 左上角
            btm_right = (sX1 + min_loc[0] + width + 4, sY1 + min_loc[1] + height + 4) # 右下角
            label_loc = (top_left[0], top_left[1] - 10)       # 标签位置
        else:
            top_left = (min_loc[0] - 5, min_loc[1] - 5) # 左上角
            btm_right = (min_loc[0] + width + 4, min_loc[1] + height + 4) # 右下角
        
        if str(index+1) in special_handle.keys():
            thresholdActual = special_handle[str(index+1)]
        else:
            thresholdActual = threshold
        # 画标准图框
        if args.gen_std_pic and min_val < thresholdActual + 0.20: # 生成对应标准图框图
            #对比失败，但差异在一定范围内，也生成标准图的矩形框
            draw_stdpic_border(save_as_std, top_left, btm_right)

        find_pos = (0, 0)
        # 生成结果对照图。失败红框，成功绿框和相应的标记数字等。
        font_scale = 0.8
        font_scale_found = 2
        font_scale_std = 3
        if min_val > thresholdActual:
            print_to_log("^^^^差异率大于阈值{:.2%}，对比失败^^^^".format(thresholdActual))
            print_to_log("ERROR: not find target area!!!")
            cv2.rectangle(cmp_pic, top_left, btm_right, RED, 2)
            if next_step_point != (0, 0) and thresholdActual != 0.004 and shift_tolerance != 0: # 非全图查找（平移相等）
                if next_step_point[0] > loc[0] and next_step_point[0] < loc[2] and next_step_point[1] > loc[1] and next_step_point[1] < loc[3] or index+1 == binding_area:
                    miss_next_step = True
                    majorAreaIsExist = True
                    #下一步要点击的位置标记**
                    cv2.putText(cmp_pic, "*{}.N*".format(index+1), label_loc, font, font_scale, RED, 2)
                else:
                    cv2.putText(cmp_pic, "{}.N".format(index+1), label_loc, font, font_scale, RED, 2)
            else:
                cv2.putText(cmp_pic, "Not Found", top_left, font, 2, RED, 2)
            diffCnt+=1
        else:
            cv2.rectangle(cmp_pic, top_left, btm_right, GREEN, 2)
            if next_step_point != (0, 0) and thresholdActual != 0.004 and shift_tolerance != 0:
                areaShift = (min_loc[0]-(loc[0]-sX1), min_loc[1]-(loc[1]-sY1))
                if areaShift[0] > 30 or areaShift[0] < -30 or areaShift[1] > 30 or areaShift[1] < -30:
                    print_to_log('^^^^对比通过，但是元素位置偏移严重！！！')
                if next_step_point[0] > loc[0] and next_step_point[0] < loc[2] and next_step_point[1] > loc[1] and next_step_point[1] < loc[3] or index+1 == binding_area:
                    #下一步要点击的位置标记**
                    majorAreaShift = areaShift
                    majorAreaIsExist = True
                    cv2.putText(cmp_pic, f"*{index+1}.Y*", label_loc, font, font_scale, GREEN, 2)
                else:
                    cv2.putText(cmp_pic, f"{index+1}.Y", label_loc, font, font_scale, GREEN, 2)
            else:
                cv2.putText(cmp_pic, "Found", min_loc, font, font_scale_found, GREEN, 2)
                find_pos = (round(width / 2) + min_loc[0], round(height / 2) + min_loc[1])

    if next_step_point != (0, 0) and majorAreaIsExist == False and thresholdActual == 0.004: # 平移相等，没有下一个对比位置
        print_to_log(f'用例需要整改: 下一步点击位置（{next_step_point}）不在对比区域内')

    if args.gen_std_pic:
        os.remove(cmp_pic_path)
        cv2.imwrite(cmp_pic_path.split('.jpeg')[0] + '.png', save_as_std, [cv2.IMWRITE_PNG_COMPRESSION, 0])
    
    desc_img = draw_top_bar(cW+sW, desc)
    cv2.putText(std_pic, 'Std', (10, 80), font, font_scale_std, RED, 3)
    cv2.putText(cmp_pic, 'Cmp', (10, 80), font, font_scale_std, RED, 3)
    # 拼接图片
    cmp_stich = np.concatenate([std_pic, cmp_pic], axis=1)
    cmp_result = np.concatenate([desc_img, cmp_stich], axis=0)
    cv2.imwrite(result_file, cmp_result, [cv2.IMWRITE_JPEG_QUALITY, 30])
    
    # 没找到下一步位置 或 所有区域对比失败
    return miss_next_step or (diffCnt > 0 and diffCnt >= len(check_loc_array)), majorAreaShift, find_pos

def pic_diff_check(std_pic_path, cmp_pic_path, check_loc_array, res_file, threshold=0.004,
                   next_step_point=(0, 0), binding_area=0, special_handle={},
                   shift_tolerance=0, desc='NO_DESCRIBE@NEXT@NO DESCRIBE', mask_array=[]):
    '''
    @check_loc_array: 要对比的区域坐标列表。\n
    @next_step_point: 主要对比区域，也即下一次要点击的区域。\n
    @binding_area: 绑定的区域。\n
    @std_pic_path: 标准图路径。\n
    @cmp_pic_path: 要对比的截图。\n
    '''
    if (not os.path.exists(std_pic_path)):
        print_to_log(f'标准图不存在：{std_pic_path}')
    if (not os.path.exists(cmp_pic_path)):
        print_to_log(f'对比图不存在：{cmp_pic_path}')

    std_pic = cv2.imread(std_pic_path)
    cmp_pic = cv2.imread(cmp_pic_path)
    # 保持图片方向一致(视频截图可能出现旋转状态的截图)
    sH, sW, _ = std_pic.shape
    cH, cW, _ = cmp_pic.shape 
    if sH < sW:
        std_pic = cv2.transpose(std_pic)
        sH, sW = sW, sH
    if cH < cW:
        cmp_pic = cv2.transpose(cmp_pic)
        cH, cW = cW, cH
    
    diffCnt = 0
    miss_next_step = False
    majorAreaShift = (0, 0)
    majorAreaIsExist = False
    save_as_std = cmp_pic.copy()
    font = cv2.FONT_HERSHEY_COMPLEX
    font_scale = 0.8
    font_scale_found = 2
    font_scale_std = 3

    # if len(check_loc_array) < 1:
    #     print_to_log("ERROR: Input params invaild, Picture Match Only Support One Location!!!")
    #     return
    # 依次处理每一个要对比的区域
    find_pos = (0, 0)
    for index, loc in enumerate(check_loc_array):
        # 获取实际比较的区域
        v = shift_tolerance
        sY1 = 0 if loc[1] <= v else loc[1] - v
        sY2 = device.H if loc[3] + v > device.H else loc[3] + v
        sX1 = 0 if loc[0] <= v else loc[0] - v
        sX2 = device.W if loc[2] +v > device.W else loc[2] + v
        if shift_tolerance != 0 and threshold != 0.004: # 非平移相等
            crop_cmp_pic = cmp_pic[sY1:sY2+1, sX1:sX2+1]
        else:
            crop_cmp_pic = cmp_pic
        crop_std_pic = std_pic[loc[1] : loc[3] + 1, loc[0]: loc[2] + 1]
        height, width, _ = crop_std_pic.shape
        # 对比图片
        if len(mask_array) != 0:
            results = cv2.matchTemplate(crop_cmp_pic, crop_std_pic, cv2.TM_SQDIFF_NORMED, mask=mask_array[index])
        else:
            results = cv2.matchTemplate(crop_cmp_pic, crop_std_pic, cv2.TM_SQDIFF_NORMED)
        # 获取匹配结果中的最小值，最大值，最小坐标和最大坐标
        # cv2.TM_SQDIFF_NORMED 最小值为最佳匹配，最小坐标为最佳匹配处的坐标
        min_val, _, min_loc, _ = cv2.minMaxLoc(results) # 平移相等使用min_val
        print_to_log(f"差异值: {min_val}")
        print_to_log(f"匹配到位置: {min_loc} {width} {height}")
        
        if threshold != 0.004: # 非平移相等
            # 手动计算差异率
            _matched_area = crop_cmp_pic[min_loc[1] : min_loc[1] + height,
                                        min_loc[0] : min_loc[0] + width] ## 匹配到的区域
            data=np.abs((crop_std_pic.astype(np.int32) - _matched_area.astype(np.int32)))
            vcnt = (data > 30).any(axis=2).sum()
            min_val = vcnt / (height * width)
        print_to_log(f"检查的位置{index}：{loc}， 向右向下偏移: {min_loc[0]-(loc[0]-sX1), min_loc[1]-(loc[1]-sY1)}， 差异率: {min_val:.2%} ")
        # 加上边框后的区域坐标
        if threshold != 0.004 and shift_tolerance != 0: # 非平移相等或#OPEN APP#
            top_left = (min_loc[0] + sX1 - 5, min_loc[1] + sY1 - 5) # 左上角
            btm_right = (sX1 + min_loc[0] + width + 4, sY1 + min_loc[1] + height + 4) # 右下角
            label_loc = (top_left[0], top_left[1] - 10)       # 标签位置
        else:
            top_left = (min_loc[0] - 5, min_loc[1] - 5) # 左上角
            btm_right = (min_loc[0] + width + 4, min_loc[1] + height + 4) # 右下角
        
        if str(index+1) in special_handle.keys():
            thresholdActual = special_handle[str(index+1)]
        else:
            thresholdActual = threshold
        # 画标准图框
        if args.gen_std_pic and min_val < thresholdActual + 0.20: # 生成对应标准图框图
            #对比失败，但差异在一定范围内，也生成标准图的矩形框
            draw_stdpic_border(save_as_std, top_left, btm_right)

        # 生成结果对照图。失败红框，成功绿框和相应的标记数字等。
        if min_val > thresholdActual:
            print_to_log("^^^^差异率大于阈值{:.2%}，对比失败^^^^".format(thresholdActual))
            print_to_log("ERROR: not find target area!!!")
            cv2.rectangle(cmp_pic, top_left, btm_right, RED, 2)
            if thresholdActual != 0.004 and shift_tolerance != 0: # 非全图查找（平移相等）
                if next_step_point[0] > loc[0] and next_step_point[0] < loc[2] and next_step_point[1] > loc[1] and next_step_point[1] < loc[3] or index+1 == binding_area:
                    miss_next_step = True
                    majorAreaIsExist = True
                    #下一步要点击的位置标记**
                    cv2.putText(cmp_pic, "*{}.N*".format(index+1), label_loc, font, font_scale, RED, 2)
                else:
                    cv2.putText(cmp_pic, "{}.N".format(index+1), label_loc, font, font_scale, RED, 2)
            else:
                cv2.putText(cmp_pic, "Not Found", top_left, font, 2, RED, 2)
            diffCnt+=1
        else:
            cv2.rectangle(cmp_pic, top_left, btm_right, GREEN, 2)
            if thresholdActual != 0.004 and shift_tolerance != 0: # 非平移相等
                areaShift = (min_loc[0]-(loc[0]-sX1), min_loc[1]-(loc[1]-sY1))
                if areaShift[0] > 30 or areaShift[0] < -30 or areaShift[1] > 30 or areaShift[1] < -30:
                    print_to_log('^^^^对比通过，但是元素位置偏移严重！！！')
                if next_step_point[0] > loc[0] and next_step_point[0] < loc[2] and next_step_point[1] > loc[1] and next_step_point[1] < loc[3] or index+1 == binding_area:
                    #下一步要点击的位置标记**
                    majorAreaShift = areaShift
                    majorAreaIsExist = True
                    cv2.putText(cmp_pic, f"*{index+1}.Y*", label_loc, font, font_scale, GREEN, 2)
                else:
                    cv2.putText(cmp_pic, f"{index+1}.Y", label_loc, font, font_scale, GREEN, 2)
            else:
                cv2.putText(cmp_pic, "Found", min_loc, font, font_scale_found, GREEN, 2)
                find_pos = (round(width / 2) + min_loc[0], round(height / 2) + min_loc[1])

    if majorAreaIsExist == False and threshold == 0.004: # 平移相等，没有下一个对比位置
        print_to_log(f'用例需要整改: 下一步点击位置（{next_step_point}）不在对比区域内')
        
    if args.gen_std_pic:
        cv2.imwrite(cmp_pic_path.split('.jpeg')[0]+'.png', save_as_std, [cv2.IMWRITE_PNG_COMPRESSION, 0])
        os.remove(cmp_pic_path)

    desc_img = draw_top_bar(cW+sW, desc)
    cv2.putText(std_pic, "Std", (10, 80), font, font_scale_std, RED, 3)
    cv2.putText(cmp_pic, "Cmp", (10, 80), font, font_scale_std, RED, 3)
    # 拼接图片
    cmp_stich = np.concatenate([std_pic, cmp_pic], axis=1)
    cmp_result = np.concatenate([desc_img, cmp_stich], axis=0)
    cv2.imwrite(res_file, cmp_result, [cv2.IMWRITE_JPEG_QUALITY, 30])
    
    # 没找到下一步位置 或 所有区域对比失败
    return miss_next_step or (diffCnt > 0 and diffCnt >= len(check_loc_array)), majorAreaShift, find_pos


def many_areas_pic_match(std_pic_path, cmp_pic_path, check_loc_array,
                         next_step_point = (0, 0), binding_area=0,
                         res_file = "find_result.jpeg", desc='NO DISCRIBE@NEXT@NO DISCRIBE',
                         mask_array = []):
    if len(check_loc_array) < 1:
        print_to_log(f'对比区域数量不能小于1，函数执行失败。')
        return None

    std_pic = cv2.imread(std_pic_path)
    cmp_pic = cv2.imread(cmp_pic_path)

    sH, sW, _ = std_pic.shape
    cH, cW, _ = cmp_pic.shape
    if sH < sW:
        std_pic = cv2.transpose(std_pic)
        sH, sW = sW, sH
    if cH < cW:
        cmp_pic = cv2.transpose(cmp_pic)
        sH, sW = sW, sH

    diffCnt = 0
    majorAreaShift = (0, 0)
    majorAreaIsExist = False

    save_as_std = cmp_pic.copy()
    for index, loc in enumerate(check_loc_array):
        # 截取标准图相同区域
        crop_std_pic = std_pic[loc[1]:loc[3]+1, loc[0]:loc[2]+1]
        height, width, _ = crop_std_pic.shape
        if len(mask_array) != 0:
            results = cv2.matchTemplate(cmp_pic, crop_std_pic, cv2.TM_SQDIFF_NORMED, mask=mask_array[index])
        else:
            results = cv2.matchTemplate(cmp_pic, crop_std_pic, cv2.TM_SQDIFF_NORMED)
        #获取匹配结果中的最小值、最大值、最小值坐标和最大值坐标
        _, _, min_loc, _ = cv2.minMaxLoc(results)
        _matched_area = cmp_pic[min_loc[1] : min_loc[1]+ height,
                                min_loc[0] : min_loc[0]+ width]
        # 计算差异率
        data = np.abs((crop_std_pic.astype(np.int32) - _matched_area.astype(np.int32)))
        vcnt = (data > 30).any(axis=2).sum()
        value = vcnt / (height * width)
        print_to_log(f"检查的位置：{loc}， 向右向下偏移: {(min_loc[0]-loc[0], min_loc[1]-loc[1])}， 差异率: {value:.2%}")
        # 加上边框后的区域坐标
        top_left  = (min_loc[0] - 5, min_loc[1] - 5)
        btm_right = (min_loc[0] + width + 4, min_loc[1] + height + 4)
        addLablePoint = (top_left[0], btm_right[1]-25)

        if args.gen_std_pic and value < 0.15:
            #即使该区域对比结果超过限制，也生成标准图的矩形框
            draw_stdpic_border(save_as_std, top_left, btm_right)

        thresholdActual = 0.09
        font = cv2.FONT_HERSHEY_COMPLEX
        # findPos = (0, 0)
        if value > thresholdActual:
            print_to_log(f"^^^^差异率大于阈值{thresholdActual:.2%}，对比失败^^^^")
            cv2.rectangle(cmp_pic, top_left, btm_right, RED, 2)
            if next_step_point[0] > loc[0] and next_step_point[0] < loc[2] and \
                next_step_point[1] > loc[1] and next_step_point[1] < loc[3] or \
                index+1 == binding_area: # ? bind 是做什么的？
                cv2.putText(cmp_pic, "*{}.N*".format(index+1), addLablePoint, font, 1, RED, 2)
                miss_next_step = 1
                majorAreaIsExist = True
            else:
                cv2.putText(cmp_pic, "{}.N".format(index+1), addLablePoint, font, 1, RED, 2)
            diffCnt+=1
        else:
            areaShift = (min_loc[0]-loc[0], min_loc[1]-loc[1])
            if next_step_point[0] > loc[0] and next_step_point[0] < loc[2] and \
                next_step_point[1] > loc[1] and next_step_point[1] < loc[3] or \
                index+1 == binding_area:
                #下一步要点击的位置标记**
                majorAreaShift = areaShift
                majorAreaIsExist = True
                cv2.putText(cmp_pic, "*{}.Y*".format(index+1), addLablePoint, font, 1, GREEN, 2)
            else:
                cv2.putText(cmp_pic, "{}.Y".format(index+1), addLablePoint, font, 1, GREEN, 2)
            cv2.rectangle(cmp_pic, top_left, btm_right, GREEN, 2)

    if args.gen_std_pic:
        os.remove(cmp_pic_path)
        cv2.imwrite(cmp_pic_path.split('.jpeg')[0]+'.png', save_as_std, [cv2.IMWRITE_PNG_COMPRESSION, 0])

    if next_step_point != (0, 0) and majorAreaIsExist == False:
        print_to_log(f'用例需要整改: 下一步点击位置（{next_step_point}）不在对比区域内')

    desc_img = draw_top_bar(cW+sW, desc)
    cv2.putText(std_pic, "Std", (10, 80), font, 3, RED, 3)
    cv2.putText(cmp_pic, "Cmp", (10, 80), font, 3, RED, 3)
    # 拼接图片
    cmp_stich  = np.concatenate([std_pic, cmp_pic], axis=1)
    cmp_result = np.concatenate([desc_img, cmp_stich], axis=0)
    cv2.imwrite(res_file, cmp_result, [cv2.IMWRITE_JPEG_QUALITY, 30])
    # 对比失败区域数量大于一半
    return miss_next_step or (diffCnt > len(check_loc_array) // 2), majorAreaShift
    

def _init_case_log_path(run_log_path, snap_shot_path, cmp_result_path):
    '''初始化单个用例的 log 目录。'''
    if os.path.exists(run_log_path):
        shutil.rmtree(run_log_path)
    if os.path.exists(snap_shot_path):
        shutil.rmtree(snap_shot_path)
    if os.path.exists(cmp_result_path):
        shutil.rmtree(cmp_result_path)
    os.makedirs(run_log_path)
    os.makedirs(snap_shot_path)
    os.makedirs(cmp_result_path)


def _check_one_page(snap_shot_path, cmp_result_path,
                   app_pic_path, next_step_desc,
                   app_pic_area, page_index,) -> bool:
    # 截图
    shot_name = f"page_{page_index}.jpeg"
    snap_shot = os.path.join(snap_shot_path, shot_name)
    print_to_log(f'获取截图：{snap_shot}')
    device.get_shot(shot_name, snap_shot)
    # 比较
    cmp_result = os.path.join(cmp_result_path, shot_name)
    # 经过简单测试，只要在本页匹配成功，差异率不会大于 0.04
    _, _, app_pos = pic_diff_check(app_pic_path, snap_shot, app_pic_area, cmp_result, threshold=0.04, \
        desc='start app'+'@NEXT@'+next_step_desc)
    if app_pos == (0, 0):
        return False
    # 打开应用
    device.click(app_pos)
    return True


def traverse_all_page(snap_shot_path, cmp_result_path,
                      app_pic_path, next_step_desc, app_pic_area):
    '''遍历匹配所有页面。
    @snap_shot_path: 存放截图的目录 \n
    注意：页面不能大于5个，从第一页开始匹配。
    '''
    for i in range(5):
        if _check_one_page(snap_shot_path, cmp_result_path,
                          app_pic_path, next_step_desc,
                          app_pic_area, page_index=i+1):
            time.sleep(1) # 打开应用动画时间
            return True
        device.swipe((600, 900), (150, 900), 100) # 下一页
        time.sleep(0.5) # 确保滑动动画完成
    return False


def run_case(case:str, run_map:dict, failed_retry):
    data = xlrd.open_workbook(case)
    case_name = case.split(os.sep)[-2]
    if data.sheet_names()[0] != "自动化验证":
        print_to_log('获取 xlsx 表的表单不匹配...')
        html_output()
        sys.exit(-1)
    table = data.sheets()[0] # 获取测试用例表格
    nrows = table.nrows      # 获取表的行数
    
    case_log_path = generate_log_path(case, failed_retry)
    run_log_path = os.path.join(case_log_path, 'run_log')
    snap_shot_path = os.path.join(case_log_path, 'snap_shot_result')
    cmp_result_path = os.path.join(case_log_path, 'cmp_result')
    _init_case_log_path(run_log_path, snap_shot_path, cmp_result_path)
    
    app_pic = '' # 要打开应用的图标
    case_begin_index = 0 # 用例描述开始的行
    # 正式跑用例之前的预处理
    for n in range(nrows):
        if table.row_values(n)[0].startswith('应用名称'):
            case_begin_index = n
        if table.row_values(n)[0] == '依赖用例':
            if not run_dep_case(table.row_values(n)[1].split('\\'), case_name, case, run_map, failed_retry):
                return False
        if table.row_values(n)[0] == '应用图标':
            app_pic = table.row_values(n)[1]
        if table.row_values(n)[0] == '步骤':
            break
    
    print_to_log('\n\n-------------------------------------------------------------------------------------------', 'notime')
    print_to_log(f'执行用例 {case}', 'notime')
    print_to_log('-------------------------------------------------------------------------------------------', 'notime')
    print_to_log(f'{table.row_values(case_begin_index)[0]}')
    set_log()
    
    to_error_process = False
    top0_end_process = False
    
    get_pos_from_last_step = ''
    get_shift_from_last_step = (0, 0)
 
    retry_cnt = 0
    retry_step = 0
    
    i = n + 1 # 第一个步骤的行
    while 1:        
        # 读取每一步骤的操作数据
        step_num = table.row_values(i)[0]
        step_desc = table.row_values(i)[1]
        action_type = table.row_values(i)[2]
        delay_time = table.row_values(i)[3]
        next_step_desc = table.row_values(i + 1)[1]
        # 打开应用
        if app_pic != '' and step_desc.startswith('#OPEN APP#'):
            print_to_log('\n\n', 'notime')
            print_to_log('#' * 66, 'notime')
            print_to_log(f'{case_name} || STEP{int(step_num)} ||: {step_desc.split("#")[-1]}')
            print_to_log('#' * 66, 'notime')
            app_pic_path = os.path.join(os.path.dirname(case), app_pic)
            app_pic_area = get_cmp_area(app_pic_path)

            device.back_home()
            time.sleep(0.5)
            found_app = False
            max_try = 3
            while not found_app and max_try:
                for _ in range(5): # 确保在第一页
                    device.swipe((150, 900), (600, 900), 100)
                    time.sleep(0.3) # 确保动效完毕
                time.sleep(0.5) # 确保动效完毕
                found_app = traverse_all_page(snap_shot_path, cmp_result_path,
                    app_pic_path, next_step_desc, app_pic_area)
                max_try -= 1
            if not found_app:
                to_error_process = True
            else: # 打开 app 后执行延时操作
                time.sleep(delay_time + 0.5)
            i += 1
            continue
        
        if step_num == '' or step_num == 'END':
            if not to_error_process:
                run_map[case_name] = 'successed'
                print_to_log(f'\n++++++++++++++++++++++ [执行结果] 状态：{run_map[case_name]}，用例 {case_name} 测试通过 +++++++++++++++++++++++++', 'notime')
            else:
                run_map[case_name] = 'failed'
                get_log(run_log_path)
                print_to_log(f'\n++++++++++++++++++++++ [执行结果] 状态：{run_map[case_name]}，用例 {case_name} 失败！！！+++++++++++++++++++++++++', 'notime')
            # print_to_log(f'\n####################################### run case: {case_name} finish #####################################', 'notime')
            save_run_map_to_device(run_map)
            device.back_home()
            time.sleep(1)
            return not to_error_process
        
        if int(step_num) + n != i:
            print_to_log('ERROR：步骤标号错误，退出该条用例执行，请检查用例！！！')
            html_output()
            sys.exit(-1)
        
        if retry_step == int(step_num) + 1:
            action_type = '无动作'
            print_to_log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
            print_to_log(f'>>> STEP{retry_step}失败，比较上一步截图，确认是否是动作未起效，是则重试，否则跳出该用例执行')
            print_to_log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
        elif to_error_process == False or step_desc.startswith('#END#'):
            print_to_log('\n\n', 'notime')
            print_to_log('##################################################################', 'notime')
            print_to_log(f'{case_name} || STEP{int(step_num)} ||: {step_desc.split("#")[-1]}')
            print_to_log('##################################################################', 'notime')    
        
        touch_pos_or_cmd = table.row_values(i)[4]
        cmp_type = table.row_values(i)[5]
        cmp_area_str = table.row_values(i)[7]
        special_process = table.row_values(i)[8]
        next_step_action_type = table.row_values(i + 1)[2]
        next_step_touch_pos_or_cmd = table.row_values(i + 1)[4]

        # 特殊处理收集
        touch_binding = 0
        special_threshold = {} # todo: 重命名为 special_threshold
        _special_cmds = special_process.split(';')
        for _cmd in _special_cmds:
            if 'TouchBinding:' in _cmd:
                touch_binding = int(_cmd.split(':')[1])
            elif 'SpecialThreshold:' in _cmd:
                _cmd_t = _cmd.split(':')[1] # eg, 2-0.15
                special_threshold[_cmd_t.split('-')[0]] = float(_cmd_t.split('-')[1])

        expect_pic_name = table.row_values(i)[6]
        expect_pic_path = os.path.join(os.path.dirname(case), expect_pic_name)
        # last_expect_pic_name = table.row_values(i - 1)[6]
        # last_expect_pic_path = os.path.join(os.path.dirname(case), last_expect_pic_name)
        
        if args.is_top0 and step_desc.startswith('#TOP0 END#'): # ? 有什么用？
            top0_end_process = True
        # 遇到top0异常，跳过正常步骤，直到 #END# 步骤
        if top0_end_process == True and not step_desc.startswith('#END#'):
            print_to_log(f'>>>TOP0 该用例执行到这里结束，跳过{int(step_num)}---{step_desc}')
            i += 1
            continue
        # 遇到异常，跳过正常步骤，直到 #END# 步骤
        if to_error_process == True and not step_desc.startswith('#END#'):
            print_to_log(f'>>>跳过{step_num}---{step_desc}')
            i += 1
            continue
        
        print_to_log('>>> 开始解析基准图片的对比区域，并获取下一步点击坐标', 1)
        if cmp_type == '' or action_type == '' or delay_time == '' or cmp_area_str == '' or \
            step_desc == '' or touch_pos_or_cmd == '' or expect_pic_name == '':
            print_to_log('ERROR：配置参数存在空白，退出用例执行！！！')
            html_output()
            sys.exit(-1)
        if next_step_touch_pos_or_cmd != '/' and cmp_type == '平移相等': # ? 平移相等就是完全相等？
            print_to_log('ERROR：当前步骤是平等相移不需要配置下一步点击坐标，退出用例执行！！！')
            html_output()
            sys.exit(-1)
        
        cmp_area = []
        # 获取指定对比区域
        if cmp_area_str != '/':
            _array = cmp_area_str.split() # 一对坐标一组（4个数字）
            if len(_array) % 4 != 0: # 检查输入是否正确
                print_to_log(f'ERROR：对比区域输入坐标数（{len(_array)}）错误。')
                html_output()
                sys.exit(-1)
            _array_nums = len(_array) / 4
            print_to_log(f'解析到指定坐标对比区域 {_array_nums} 个')
            for _j in range(_array_nums):
                cmp_area.append([int(_array[_j * 4]), int(_array[_j * 4 + 1]), \
                    int(_array[_j * 4 + 2]), int(_array[_j * 4 + 3])])
        # 获取对比区域
        if (cmp_area_str == '/' and cmp_type != '不对比') or \
            (next_step_touch_pos_or_cmd == '/' and cmp_type != '平移相等' and next_step_action_type != '无动作'):
            cmp_area = get_cmp_area(expect_pic_path)
            print_to_log(f'解析到线框对比区域{len(cmp_area)}个')
            if cmp_area_str == '/' and len(cmp_area) == 0:
                print_to_log('ERROR：没有解析到对比区域！！！')
            if next_step_touch_pos_or_cmd == '/' and cmp_type != '平移相等': # ? 
                print_to_log('ERROR：没有解析到对比区域！！！')
        # 动作命令处理
        if touch_pos_or_cmd == '/' and action_type != '无动作' and get_pos_from_last_step == '': # ? 无动作什么时候使用
            print_to_log(f'ERROR： [执行结果] 用例{case_name}失败，没解析到点击位置，退出该条用例执行！！！')
            to_error_process = True
            get_shift_from_last_step = (0, 0)
            retry_cnt = 0
            retry_step = 0
            i += 1
            continue
        
        if touch_pos_or_cmd != '/':
            if action_type != '命令':
                print_to_log(f'解析到坐标标注点击位置：{touch_pos_or_cmd}')
                print_to_log(f'带点击位置偏移量：{get_shift_from_last_step}')
            else:
                print_to_log(f'执行命令：{touch_pos_or_cmd}')
            # 轻触和长按的坐标需要预偏移处理
            if action_type == '轻触':
                touch_pos_or_cmd = f'{int(touch_pos_or_cmd.split()[0]) + get_shift_from_last_step[0]} ' \
                    f'{int(touch_pos_or_cmd.split()[1]) + get_shift_from_last_step[1]}'
                get_shift_from_last_step = (0, 0)
            if action_type == '长按':
                touch_pos_or_cmd = f'{int(touch_pos_or_cmd.split()[0]) + get_shift_from_last_step[0]} ' \
                    f'{int(touch_pos_or_cmd.split()[1]) + get_shift_from_last_step[1]} ' \
                    f'{int(touch_pos_or_cmd.split()[2])}'
                get_shift_from_last_step = (0, 0)
            print_to_log(f'加偏移后的应点击位置：{touch_pos_or_cmd}')
        elif get_pos_from_last_step != '':
            touch_pos_or_cmd = get_pos_from_last_step
            get_pos_from_last_step = ''
            print_to_log(f'解析到上一步标注点击位置：{touch_pos_or_cmd}')
        # 动作执行
        print_to_log(f'>>> 开始动作，类型：{action_type}')
        if action_type == '轻触':
            device.click(touch_pos_or_cmd.split())
        elif action_type == '轻触模板匹配返回的坐标': # ? 和轻触的区别，是否需要偏移处理
            device.click(touch_pos_or_cmd.split())
        elif action_type == '滑动':
            device.swipe(touch_pos_or_cmd)
            time.sleep(1)
        elif action_type == '长按':
            _cmd = touch_pos_or_cmd.split()
            device.hold_press(_cmd[0:2], _cmd[2])
        elif action_type == '拖拽':
            device.drag(touch_pos_or_cmd)
        elif action_type == '命令':
            # 规避应用退出桌面模糊
            if touch_pos_or_cmd.find('hdc file send -a') != -1:
                temp_cmd_list = touch_pos_or_cmd.split() # hdc file send -a local remote
                temp_cmd_list[4] = os.path.join(cwd, temp_cmd_list[4])
                touch_pos_or_cmd = ' '.join(temp_cmd_list)
            if touch_pos_or_cmd.find('kill') != -1 and touch_pos_or_cmd.find('-9') != -1:
                device.back_home()
            if touch_pos_or_cmd.find('hdc_std ') != -1:
                touch_pos_or_cmd.replace('hdc_std', f'hdc_std -t {device_num}')
            if touch_pos_or_cmd.find('hdc ') != -1:
                touch_pos_or_cmd.replace('hdc', f'hdc -t {device_num}')
            execute(touch_pos_or_cmd, '动作:' + action_type)
        elif action_type == '无动作':
            print_to_log('动作:无动作')
        elif action_type != '无动作':
            print_to_log('ERROR：动作类型无法识别，退出用例执行！！！')
            html_output()
            sys.exit(-1)
        # 动作执行后延时获取截图
        time.sleep(delay_time + RK3568_DELAY)
        print_to_log(f'>>> 开始截图对比，方式：{cmp_type}')
        _shot_name = f'r{retry_cnt}_step{int(step_num)}.jpeg'
        shot_path = os.path.join(snap_shot_path, _shot_name)
        device.get_shot(_shot_name, shot_path) # 应用更新这个应用打开软件时存在无法截图的问题
        
        cmp_file_n = f'step{int(step_num)}_retry{retry_cnt}_{expect_pic_name.split(".")[0]}.jpeg'
        cmp_result = os.path.join(cmp_result_path, cmp_file_n)
        major_pos = (0, 0)
        
        if next_step_action_type == '轻触' and next_step_touch_pos_or_cmd != '/':
            major_pos = (int(next_step_touch_pos_or_cmd.split()[0]), int(next_step_touch_pos_or_cmd.split()[1]))
        
        # dif_cnt = 0
        match_failed = True
        skip_cmp = False # 标记是否不对比
        major_area_shift = (0, 0)
        next_touch_pos = (0, 0)
        shift = args.shift_tolerance
        desc = step_desc+'@NEXT@'+next_step_desc
        if cmp_type == '等于':
            match_failed, major_area_shift, _ = pic_diff_check(expect_pic_path, shot_path, cmp_area, cmp_result, 0.03, major_pos, touch_binding, 
                special_threshold, shift, desc=desc)
        elif cmp_type == '约等':
            match_failed, major_area_shift, _ = pic_diff_check(expect_pic_path, shot_path, cmp_area, cmp_result, 0.10, major_pos, touch_binding, 
                special_threshold, shift, desc=desc)
        elif cmp_type == '相似':
            match_failed, major_area_shift, _ = pic_diff_check(expect_pic_path, shot_path, cmp_area, cmp_result, 0.15, major_pos, touch_binding, 
                special_threshold, shift, desc=desc)
        elif cmp_type == '平移相等':
            print_to_log(cmp_area)
            _, _, next_touch_pos = pic_diff_check(expect_pic_path, shot_path, cmp_area, cmp_result, threshold=0.004, desc=desc)
            if next_touch_pos == (0, 0):
                match_failed = True
        elif cmp_type == '多区域平移相等':
            match_failed, major_area_shift = many_areas_pic_match(expect_pic_path, shot_path, 
                check_loc_array=cmp_area, next_step_point=major_pos, binding_area=touch_binding,
                res_file=cmp_result, desc=desc)
        elif cmp_type == '不对比':
            skip_cmp = True
            match_failed, major_area_shift = False, (0, 0)

        # '不对比'不会生成对比图, 跳过对比结果分析
        if skip_cmp == True:
            skip_cmp = False
            get_shift_from_last_step = major_area_shift
            i += 1
            continue
        # 对比结果分析
        expect_retry_cnt = 1
        if match_failed: # 匹配失败处理
            # 第一步失败直接退出执行该条用例
            if int(step_num) == 1: # retry_step == 0 and
                print_to_log(f'>>>>>>>>>>{case} STEP1 动态结束，对比结果：失败！！！\n对比图路径：{cmp_result}')
                to_error_process = True
                get_pos_from_last_step = ''
                get_shift_from_last_step = (0, 0)
                retry_cnt = 0
                retry_step = 0
                i += 1
                continue
            # 第一次失败，不是第一步且不是结束步骤。跳到前一步判断是否点击未起效，是则重试
            if retry_step == 0 and not step_desc.startswith('#END#'):
                retry_step = int(step_num)
                retry_cnt += 1
                next_touch_pos = (0, 0)
                i -= 1
                continue
            # 重试失败，计数
            if retry_step == int(step_num) and retry_cnt < expect_retry_cnt:
                retry_cnt += 1
                next_touch_pos = (0, 0)
                i -= 1
                continue
                pass
            #重试多次失败，退出重试
            if retry_step == int(step_num) and retry_cnt == expect_retry_cnt:
                print_to_log(f'ERROR：STEP{int(step_num)+1}多处对比失败或下一步点击区域对比失败，阻塞当前用例的下一步验证')
                print_to_log(f'>>>>>>>>>>>>> 已重试STEP{int(step_num)}，仍然失败，退出当前用例执行')
                print_to_log(f'这里查看对比详情：log\TestCase\App...\App_..._001\App_...\cmp_result')
                print_to_log(f'>>>>>>>>>>>>>{case_name} STEP{int(step_num)}动作结束，对比结果:失败！！！ \n对比图路径：{cmp_result}')
                to_error_process = True
                get_pos_from_last_step = ''
                get_shift_from_last_step = (0, 0)
                retry_cnt = 0
                retry_step = 0
                i += 1
                continue
            # 重试上一步依旧失败，退出
            if retry_step == int(step_num) + 1:
                newname = os.path.join(os.path.dirname(cmp_result), 'check_last_step_' + os.path.basename(cmp_result))
                if os.path.exists(newname):
                    os.remove(newname)
                os.rename(cmp_result, newname)
                print_to_log(f'ERROR：STEP{int(step_num)+1}多处对比失败或下一步点击区域对比失败，阻塞当前用例的下一步验证')
                print_to_log(f'>>>>>>>>>>>>> 已重试STEP{int(step_num)}，确认不是点击未起效，退出当前用例执行！！！')
                print_to_log(f'>>>>>>>>>>>>>{case_name} STEP{int(step_num)+1}动作结束，对比结果:失败！！！ \n对比图路径：{cmp_result}')
                to_error_process = True
                get_pos_from_last_step = ''
                get_shift_from_last_step = (0, 0)
                retry_cnt = 0
                retry_step = 0
                i += 1
                continue
        else:
            #跳到上一步，确认可能是点击未起效，开始重试
            if retry_step == int(step_num) + 1: # ?
                newname = os.path.join(os.path.dirname(cmp_result), 'check_last_step_' + os.path.basename(cmp_result))
                if os.path.exists(newname):
                    os.remove(newname)
                if os.path.exists(cmp_result):
                    os.rename(cmp_result, newname)
                else:
                    print_to_log(f'ERROR: 对比结果图不存在，请检查问题。\n对比图路径：{cmp_result}')
                print_to_log(f'>>>>>>>>>>>>> STEP{int(step_num)+1}由于点击未起效失败，开始第{retry_cnt}次重试')
            if retry_step == int(step_num):
                print_to_log(f'>>>>>>>>>>>>>{case_name} 动作结束，STEP{int(step_num)}第{retry_cnt}次重试对比结果:成功 \n对比图路径：{cmp_result}')
                retry_cnt = 0
                retry_step = 0
            if retry_step == 0:
                print_to_log(f'>>>>>>>>>>>>>{case_name} STEP{int(step_num)}动作结束，对比结果:成功 \n对比图路径：{cmp_result}')

        if next_touch_pos != (0, 0):
            get_pos_from_last_step = f"{next_touch_pos[0]} {next_touch_pos[2]}"
        get_shift_from_last_step = major_area_shift
        i += 1


def run_single_case(case, run_map, failed_retry):
    # 获取测试用例名
    relative_path = os.path.relpath(os.path.dirname(case), start=os.path.dirname(cwd))
    relative_log_path = os.path.join(os.path.dirname(cwd), 'log', relative_path)
    test_name = os.path.basename(relative_log_path)

    if len(case_list) == 1 and test_name in run_map:
        del run_map[test_name]
    if test_name not in run_map:
        run_case(case, run_map, failed_retry)


def env_init(log_testcase_path, log_path, html_path):
    if not os.path.exists(log_testcase_path):
        os.makedirs(log_testcase_path)

    if os.path.exists(log_path):
        os.remove(log_path)
    if os.path.exists(html_path):
        os.remove(html_path)


def run(case_list):
    ''' case_list 为 xlsx 的路径。'''
    # global failed_retry # 失败重试次数
    failed_retry = 0 
    while failed_retry < args.retry + 1:
        if len(case_list):
            print_to_log(f'重启设备...')
            reset(args.reset_time)
        else:
            print_to_log('无待执行测试用例， 结束执行。')
            break
        run_map = read_run_map()
        print_to_log(f'\n\n\n######################### 开始用例执行 {failed_retry} #########################\n', 'notime')
        print_to_log(case_list)
        
        for case in case_list:
            run_single_case(case, run_map, failed_retry)
        
        print_to_log(f'\n\n######################### 用例执行结束 {failed_retry} #########################\n', 'notime')
        failed_list = []
        run_map = read_run_map()
        html_output() # temp
        for case in case_list:
            key = os.path.basename(os.path.dirname(case))
            if key in run_map:
                if (run_map[key] == 'failed' or run_map[key].startswith('blocked:')):
                    del run_map[key]
                    failed_list.append(all_case_dict[key])
        case_list = failed_list
        failed_retry += 1
        save_run_map_to_device(run_map)
        
        
        


if __name__ == '__main__':
    # 一些全局变量
    cwd = os.path.abspath(os.path.dirname(__file__)) # 脚本所在路径
    testcase_path = os.path.join(os.path.dirname(cwd), 'TestCase') # 测试用例根目录
    log_testcase_path = os.path.join(os.path.dirname(cwd), 'log', 'TestCase') # 运行结果存储的路径
    log_path = os.path.join(log_testcase_path, 'PreTestRun.log') # 运行 log 文件
    html_path = os.path.join(log_testcase_path, 'report.html')   # html report 路径

    # 环境检查，hdc 是否正常运行
    env_check()
    # 环境初始化，重置目录文件
    env_init(log_testcase_path, log_path, html_path)
    # 参数解析
    args = argumnet_parse()
    
    # 另一些全局变量
    device_num = args.device_num # 测试设备序列号
    device = RK3568(device_num)
    
    # 如果指定运行所有用例。测试前，先运行预处理目录的用例，wifi连接，账号登陆，预置资源等
    # 安装 weekly 版本 app
    install_weekly_app(args.fixed_app)

    # 扫描测试用例
    case_list, all_case_dict = get_case_list(args.case_path)
    
    # 运行以及重试
    run(case_list)
    
    # 设置设备休眠  
    execute(f'hdc -t {device_num} shell power-shell suspend')
    
    # 输出报告
    html_output()
